package com.fpc.cn.springbootutil.mybatisLog;

import com.fpc.cn.springbootutil.common.DbBatchUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.lang.reflect.Method;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;


/**
 * @date 2025/4/27
 * @author fupengcheng
 * @describe 拦截基础类
 */
@Slf4j
public abstract class MybatisSqlInterceptorBase implements Interceptor {
    private final static ThreadLocal<SimpleDateFormat> FORMATTER = ThreadLocal.withInitial(
            () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
    );

    private final static Object OBJ = new Object();
    private final static BlockingQueue<Item> QUEUE = new LinkedBlockingDeque<>();
    private final static String SQL_TYPE_INSERT = "INSERT";
    private final static String INSERT_MSG = "(插入语句只打印500个字符，想打印更多，可在Mapper上面添加@MpLog(insertSqlLength = 1000))";
    private final static String RESULT_NUM = "记录数:";
    private final static String DIAN = ".";

    static {
        // 起一个线程，用于打印队列中的日志
        new Thread(() -> {
            while (true) {
                try {
                    // 从队列中获取队列头的数据。
                    // 如果没有获取到数据，这个方法阻塞，直到有数据为止
                    Item item = QUEUE.take();
                    item.print(true);
                } catch (Exception e) {
                    log.error("自定义日志线程异常", e);
                }
            }
        }).start();
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取开始时间与执行时长
        long tm = System.currentTimeMillis();
        long duration = tm;
        Object result = OBJ;
        try {
            //执行模块
            result = invocation.proceed();
            duration = System.currentTimeMillis() - tm;
            return result;
        } catch (Exception e) {
            duration = System.currentTimeMillis() - tm;
            throw e;
        } finally {
            log(invocation, result, tm, duration);
        }
    }

    /**
     * 用于缓存mapper上的MpLog的注解
     */
    private final static Map<String, MpLog> MP_LOG_MAP = new ConcurrentHashMap<>(200);

    private void log(Invocation invocation, Object result, long tm, long duration) {
        if(DbBatchUtil.local() != null) {
            // 如果这个ThreadLocal里有值，就表示是批量操作，不打印日志
            return;
        }
        // 拦截逻辑
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        String id = mappedStatement.getId();
        MpLog mpLog = MP_LOG_MAP.computeIfAbsent(id, k -> {
            try {
                // 根据id加载对应的mapper类，并获取方法上的注解
                Class<?> classType = Class.forName(id.substring(0, id.lastIndexOf(DIAN)));
                String methodName = id.substring(id.lastIndexOf(DIAN) + 1);
                Method[] methods = classType.getDeclaredMethods();
                for(Method m : methods) {
                    if(methodName.equals(m.getName())) {
                        return m.getAnnotation(MpLog.class);
                    }
                }
            } catch (Exception e) {
                log.error("classTyp获取异常", e);
            }
            return null;
        });
        if(mpLog != null && !mpLog.printLog()) {
            return;
        }

        Configuration configuration = mappedStatement.getConfiguration();
        String type = mappedStatement.getSqlCommandType().name();// 获取操作类别
        Object parameter = invocation.getArgs()[1];
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        try {
            String sqlKey = mappedStatement.getId();
            String res;
            if(result instanceof List) {
                List<?> list = Util.cast(result);
                res = RESULT_NUM + list.size();
            } else if(result instanceof Map) {
                Map<?, ?> map = Util.cast(result);
                res = "记录数:1,(map)" + map.size();
            } else if(result instanceof Integer || result instanceof Long) {
                res = RESULT_NUM + result;
            } else if(result == OBJ) {
                res = "结果:异常";
            } else {
                res = RESULT_NUM + result + "(type)" + result.getClass().toString();
            }
            List<Object> parameters = values(configuration, boundSql);
            //判断是否开启sql采集,暂时不采集
            Boolean open = false;
            StringBuilder sb = formatSql(boundSql.getSql(), parameters, mpLog, type);
            String sql = sb.toString();
            if (open) {
                SqlVO sqlVO = new SqlVO();
                sqlVO.setSql(sql);
                sqlVO.setConsumingTime(duration);
                SqlStorage.setSql(sqlVO);
            }

            Item item = new Item(sql, open, mpLog, type, tm, duration, sqlKey, res);
            if(mpLog == null || mpLog.syncLog()) {
                // 往队列中添加日志数据
                if(QUEUE.offer(item)) {
                    // 添加成功则不需要再做什么了。
                    return;
                }
            }
            item.print(false);
        } catch (Exception e) {
            log.error("SQL日志打印报错：" + boundSql.getSql(), e);
        }
    }

    private List<Object> values(Configuration configuration, BoundSql boundSql) {
        List<Object> parameters = new ArrayList<>();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings == null) {
            return parameters;
        }
        //填充占位符, 目前基本不用mybatis存储过程调用,故此处不做考虑
        Object parameterObject = boundSql.getParameterObject();
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
        for (ParameterMapping parameterMapping : parameterMappings) {
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                //    参数值
                String propertyName = parameterMapping.getProperty();
                Object value;
                //    获取参数名称
                if (boundSql.hasAdditionalParameter(propertyName)) {
                    // 获取参数值
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    // 如果是单个值则直接赋值
                    value = parameterObject;
                } else {
                    value = metaObject == null ? null : metaObject.getValue(propertyName);
                }
                parameters.add(value);
            }
        }
        return parameters;
    }

    private StringBuilder formatSql(String sql, List<Object> parameters, MpLog mpLog, String type) {
        // 插入语句打印的长度
        int len = mpLog == null ? 500 : mpLog.insertSqlLength();
        //美化sql
        StringBuilder sb = new StringBuilder(sql.length() + parameters.size() * 50);
        // 用来标记只加一个空格
        boolean flag = true;
        int paramIndex = 0;
        for(int a = 0; a < sql.length(); a++) {
            char cc = sql.charAt(a);
            if(cc == ' ' || cc == '\n' || cc == '\t' || cc == '\r') {
                if(flag) {
                    // 第一次遇到需要处理的字符时，添加一个空格。后面的就直接忽略
                    flag = false;
                    sb.append(' ');
                }
                continue;
            } else if(cc == '?') {
                Object value = parameters.get(paramIndex++);
                if (value instanceof Number) {
                    sb.append(value).append(' ');
                    continue;
                }
                sb.append('\'');
                if (value instanceof Date) {
                    sb.append(FORMATTER.get().format((Date) value));
                } else {
                    sb.append(value);
                }
                sb.append('\'').append(' ');
                continue;
            }
            sb.append(cc);
            flag = true;
            // 如果注解中的打印长度设置成负数，则表示打印全部
            if(len > 0 && sb.length() >= len) {
                if(SQL_TYPE_INSERT.equals(type)) {
                    sb.append(INSERT_MSG);
                    break;
                }
            }
        }
        return sb;
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

    @Data
    private class Item {
        private String sql;
        private boolean open;
        private MpLog mpLog;
        private String type;
        private long tm;
        private long duration;
        private String sqlKey;
        private String res;


        public Item(
                String sql, boolean open, MpLog mpLog, String type,
                long tm, long duration, String sqlKey, String res
        ) {
            this.sql = sql;
            this.open = open;
            this.mpLog = mpLog;
            this.type = type;
            this.tm = tm;
            this.duration = duration;
            this.sqlKey = sqlKey;
            this.res = res;
        }

        private void print(boolean flag) {
            log.info("======>\n类别: {}{}\t{}\t开始时间:{}\t时长:{}ms\nsqlKey:{}\nSQL:{}\nopen:{}\n<======",
                    type, flag ? "(异步)" : "(同步)", res, FORMATTER.get().format(new Date(tm)), duration, sqlKey,
                    sql, open
            );
        }
    }
}
